Skip to content

feat: export useDrawerContext #467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jasongerbes
Copy link

Vaul doesn't currently export the useDrawerContext hook, which would be helpful for getting the direction of the <Drawer.Root> to style the <Drawer.Content> appropriately.

For context, I'm updating my instance of the shadcn/ui Drawer component to support all directions (similar to the Sheet component).

This PR exports useDrawerContext and DrawerContext, and includes some format fixes for the new JSDocs.

@@ -1112,3 +1112,5 @@ export const Drawer = {
Title: DialogPrimitive.Title,
Description: DialogPrimitive.Description,
};

export { DrawerContext, useDrawerContext };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to export DrawerContext? This can't be used externally.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emilkowalski Hi, Is it any way to get access to context to close drawer?

@jasongerbes
Copy link
Author

Hey @emilkowalski, you're right that DrawerContext can't currently be used externally.

My use case requires knowing the value of the direction prop to position the Drawer.Handle appropriately:

  • bottom - show Drawer.Handle at the top of the drawer.
  • top - show Drawer.Handle at the bottom of the drawer.
  • left or right - hide Drawer.Handle and show Drawer.Close instead.

I'm currently using a custom DrawerContext to share the direction prop with children of the Drawer.Root.

Here's the full implementation for context. It builds upon the shadcn/ui Drawer component.

components/ui/drawer.tsx
'use client';

import * as React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cva } from 'class-variance-authority';
import { Drawer as DrawerPrimitive } from 'vaul';

import { cn } from '@/lib/utils';

type DrawerContextValue = Pick<DrawerProps, 'direction'>;
const DrawerContext = React.createContext<DrawerContextValue>({});

const useDrawerContext = () => {
  const context = React.useContext(DrawerContext);
  if (!context) {
    throw new Error('useDrawerContext must be used within a Drawer.Root');
  }
  return context;
};

export type DrawerProps = React.ComponentProps<typeof DrawerPrimitive.Root>;

const Drawer = ({
  shouldScaleBackground = true,
  direction = 'bottom',
  ...props
}: DrawerProps) => (
  <DrawerContext.Provider value={{ direction }}>
    <DrawerPrimitive.Root
      shouldScaleBackground={shouldScaleBackground}
      direction={direction}
      {...props}
    />
  </DrawerContext.Provider>
);
Drawer.displayName = 'Drawer';

const DrawerTrigger = DrawerPrimitive.Trigger;

const DrawerPortal = DrawerPrimitive.Portal;

const DrawerClose = DrawerPrimitive.Close;

const DrawerOverlay = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Overlay
    ref={ref}
    className={cn('fixed inset-0 z-50 bg-black/80', className)}
    {...props}
  />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;

const drawContentVariants = cva(
  'fixed z-50 flex flex-col bg-background outline-none',
  {
    variants: {
      direction: {
        top: 'inset-x-0 top-0 mb-24 h-auto max-h-[90%] rounded-b-[10px]',
        bottom: 'inset-x-0 bottom-0 mt-24 h-auto max-h-[90%] rounded-t-[10px]',
        left: 'inset-y-0 left-0 mr-24 w-full max-w-[85%] rounded-r-[10px] lg:max-w-3xl',
        right:
          'inset-y-0 right-0 ml-24 w-full max-w-[85%] rounded-l-[10px] lg:max-w-3xl',
      },
    },
    defaultVariants: {
      direction: 'bottom',
    },
  },
);

const DrawerContent = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => {
  const { direction } = useDrawerContext();
  const isHorizontal = direction === 'left' || direction === 'right';

  return (
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerPrimitive.Content
        ref={ref}
        className={cn(drawContentVariants({ direction }), className)}
        {...props}
      >
        {direction === 'bottom' && <DrawerHandle />}

        <div
          className={cn(
            'flex-grow overflow-y-auto',
            direction !== 'bottom' && 'pt-3',
          )}
        >
          {children}
        </div>

        {direction === 'top' && <DrawerHandle />}

        {isHorizontal && (
          <DrawerClose className="absolute right-6 top-6 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
            <Cross2Icon className="size-5" />
            <span className="sr-only">Close</span>
          </DrawerClose>
        )}
      </DrawerPrimitive.Content>
    </DrawerPortal>
  );
});
DrawerContent.displayName = 'DrawerContent';

const DrawerHandle = () => (
  <div className="pb-2 pt-1.5">
    <DrawerPrimitive.Handle />
  </div>
);

const DrawerHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('grid gap-1.5 p-4 text-left sm:px-6', className)}
    {...props}
  />
);
DrawerHeader.displayName = 'DrawerHeader';

const DrawerBody = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('mt-auto flex flex-col px-4 sm:px-6', className)}
    {...props}
  />
);
DrawerBody.displayName = 'DrawerBody';

const DrawerFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn('mt-auto flex flex-col gap-2 p-4', className)}
    {...props}
  />
);
DrawerFooter.displayName = 'DrawerFooter';

const DrawerTitle = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Title
    ref={ref}
    className={cn(
      'text-lg font-semibold leading-none tracking-tight',
      className,
    )}
    {...props}
  />
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;

const DrawerDescription = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Description
    ref={ref}
    className={cn('text-sm text-muted-foreground', className)}
    {...props}
  />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;

export {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerBody,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
};

This workaround is sufficient, so feel free to close this PR if there are specific issues with exporting DrawerContext.

@marcusforsberg
Copy link

I also have a need to access useDrawerContext to customize the behaviour of the Overlay. I have non-modal Drawer, which currently force hides the Overlay entirely. I still need the overlay to be visible on certain snap points. If I had access to the context, I could implement my own Overlay logic.

@AhmedBaset
Copy link

Is there anything blocking merging this? I have the exact use case as the author

@willalread
Copy link

Any update on this? I would also like to access the direction via the useDrawerContext hook.

@guotie
Copy link

guotie commented Jan 22, 2025

nice feat! very useful

@tomas-hartman
Copy link

I also have a need to access useDrawerContext to customize the behaviour of the Overlay. I have non-modal Drawer, which currently force hides the Overlay entirely. I still need the overlay to be visible on certain snap points. If I had access to the context, I could implement my own Overlay logic.

I second that use case. We have quite a complex use case of a non-modal drawer that involves other ui elements to be influenced by the state of expansion of the drawer, with different behavior on different snap points. Currently there is no reliable way of knowing what the actual drawer expansion is at a given moment (that I know of). We use onDrag and onRelease events to measure the state but it's suboptimal. onDrag is not reliable (sometimes does not get called) and it does not account for the subsequent transition after the drag is released near a snap point, so the other elements cannot be simply animated and it causes flickering of the affected components.

Even if the useDrawerContext in its current state was not the solution, it would be really helpful to have access to some of the internal props of the drawer from outside.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants